<?php

require_once 'components/utils/string_utils.php';
require_once 'components/utils/reflection_utils.php';
require_once 'database_engine/common_engine.php';
require_once 'database_engine/dataset_metadata.php';
require_once 'database_engine/datareader_stub.php';

class SqlUtils
{
    public static function MakeFullName($parent, $child)
    {
        if (StringUtils::IsNullOrEmpty($parent))
            return $child;
        else
            return $parent . '.' . $child;
    }
}

class PgqlUtils
{
    public static function MakeFullName($parent, $child)
    {
        return '[' . $parent . '].[' . $child . ']';
    }

    public static function SplitFullName($fullName)
    {
        $result = array();
        $names = explode('.', $fullName);
        if (count($names) == 1)
        {
            $result[] = null;
            $result[] = trim($names[0], "[]");
        }
        else
        {
            foreach($names as $name)
            {
                $result[] = trim($name, "[]");
            }
        }
        return $result;
    }
}

class SMCondition
{
    /** @var string */
    private $left;

    /** @var \ConditionOperator */
    private $operator;

    /** @var mixed */
    private $right;

    /**
     * @param FieldInfo $fieldInfo
     * @param ConditionOperator $operator
     * @param mixed $right
     */
    public function __construct($left, ConditionOperator $operator, $right)
    {
        $this->left = $left;
        $this->operator = $operator;
        $this->right = $right;
    }

    /**
     * @return string
     */
    public function GetAsSql()
    {
        $left = '';
        if (SMReflection::IsString($this->left) || SMReflection::IsNumber($this->right))
            $left = $this->left;
        elseif (SMReflection::IsArray($this->left) && $this->left['type'] == 'field')
            $left = SqlUtils::MakeFullName($this->left['parent_table'], $this->left['name']);

        $right = '';
        if (SMReflection::IsString($this->right) || SMReflection::IsNumber($this->right))
            $right = $this->right;
        elseif (SMReflection::IsArray($this->right) && $this->right['type'] == 'field')
            $right = SqlUtils::MakeFullName($this->right['parent_table'], $this->right['name']);

        return StringUtils::Format("%s %s %s",
            $left,
            $this->operator->GetShortName(),
            $right);
    }
}

class JoinType
{
    public static $Inner;

    public static function Init()
    {
        self::$Inner = new JoinType();
    }
}
JoinType::Init();

class SelectCommandBuilder
{
    private $sources;
    private $fields;
    private $joins;

    /** @var SMCondition[] */
    private $conditions;

    public function __construct()
    {
        $this->sources = array();
        $this->fields = array();
        $this->conditions = array();
        $this->joins = array();
    }

    /**
     * @param string $query
     * @return void
     */
    public function AddSourceQuery($query)
    {
        $this->sources[] = array(
            'type' => 'query',
            'sql' => $query);
    }

    /**
     * @param string $tableName
     * @param null|string $alias
     * @return void
     */
    public function AddSourceTable($tableName, $alias = null)
    {
        $this->sources[] = array(
            'type' => 'table',
            'alias' => $alias,
            'name' => $tableName);
    }

    /**
     * @return string
     */
    public function GetSql()
    {
        if ((count($this->sources) == 1) && ($this->sources[0]['type'] == 'query'))
            return $this->sources[0]['sql'];
        else
        {
            $source = $this->sources[0]['name'];
            if ($this->sources[0]['alias'] != null)
                StringUtils::AddStr($source, $this->sources[0]['alias'], ' ');
                        
            $fields = '';
            foreach($this->fields as $field)
            {
                StringUtils::AddStr(
                    $fields,
                    SqlUtils::MakeFullName($field['parent_table'], $field['name']) .
                    ($field['alias'] == null ? '' : (' AS ' . $field['alias'])),
                    ', ');
            }
            
            $joinSql = '';
            foreach($this->joins as $join)
            {
                StringUtils::AddStr($joinSql, StringUtils::Format('INNER JOIN %s %s ON %s',
                    $join['name'], $join['alias'], $join['condition']->GetAsSql()
                ), ' ');
            }

            $result = sprintf("SELECT %s FROM %s", $fields, $source);
            StringUtils::AddStr($result, $joinSql, ' ');



            $conditionSql = '';
            foreach($this->conditions as $condition)
                StringUtils::AddStr($conditionSql, '(' . $condition->GetAsSql() . ')', ' AND ');
            if ($conditionSql != '')
                $result .= ' WHERE ' . $conditionSql;
            
            return $result;
        }
    }

    public function CreateFieldInfo($fieldName, $parentTable = null)
    {
        return array(
            'type' => 'field',
            'name' => $fieldName,
            'parent_table' => $parentTable
        );
    }

    /**
     * @param string $fieldName
     * @param null|string $parentTable
     * @return void
     */
    public function AddField($fieldName, $parentTable = null, $alias = null)
    {
        $this->fields[] = array_merge($this->CreateFieldInfo($fieldName, $parentTable), array('alias' => $alias));
    }

    /**
     * @param SMCondition $condition
     * @return void
     */
    public function AddCondition(SMCondition $condition)
    {
        $this->conditions[] = $condition;
    }

    /**
     * @param JoinType $joinType
     * @param string $tableName
     * @param string $alias
     * @param SMCondition $condition
     * @return void
     */
    public function AddJoinTable(JoinType $joinType, $tableName, $alias, SMCondition $condition)
    {
        $this->joins[] = array(
            'type' => 'table',
            'name' => $tableName,
            'alias' => $alias,
            'condition' => $condition
        );
    }
}

class SelectCommand
{
    private $sources;
    private $joins;
    
    public function __construct()
    {
        $this->sources = array();
        $this->joins = array();
    }

    public final function AddSource(DataSourceMetadata $dataSourceMetadata, $alias)
    {
        $this->sources[] = array(
            'alias' => $alias,
            'metadata' => $dataSourceMetadata
        );
    }

    /**
     * @param DataSourceMetadata $dataSourceMetadata
     * @param string $alias
     * @param string $leftField
     * @param string $rightField
     * @return void
     */
    public final function AddSimpleJoin(DataSourceMetadata $dataSourceMetadata, $alias, $leftField, $rightField)
    {
        $this->joins[] = array(
            'metadata' => $dataSourceMetadata,
            'alias' => $alias,
            'left' => $leftField,
            'right' => $rightField
        );

    }

    private function GetFieldsMap()
    {
        $map = array();

        $sourceCount = 0;
        $defaultTable = null;
        
        foreach($this->sources as $source)
        {
            $sourceAlias = $source['alias'];
            if ($defaultTable == null)
                $defaultTable = $sourceAlias;

            /** @var DataSourceMetadata $sourceMetadata  */
            $sourceMetadata = $source['metadata'];
            $sourceSqlAlias = 't' . $sourceCount;

            $sourceAliasToSqlAlias[$sourceAlias] = $sourceSqlAlias;
            $sourceCount++;

            foreach($sourceMetadata->GetFieldNames() as $fieldName)
                $map[PgqlUtils::MakeFullName($sourceAlias, $fieldName)] = $fieldName;
        }

        foreach($this->joins as $join)
        {
            $joinAlias = $join['alias'];

            /** @var DataSourceMetadata $joinMetadata */
            $joinMetadata = $join['metadata'];
            $joinSqlAlias = 't' . $sourceCount;

            $sourceAliasToSqlAlias[$joinAlias] = $joinSqlAlias;

            foreach($joinMetadata->GetFieldNames() as $fieldName)
                $map[PgqlUtils::MakeFullName($joinAlias, $fieldName)] = $joinSqlAlias . '_' . $fieldName;
            
            $sourceCount++;
        }
        return array('defaultTable' => $defaultTable, 'map' => $map);
    }

    private function GetInternalFieldsMap()
    {
        $map = array();

        $sourceCount = 0;
        $defaultTable = null;

        foreach($this->sources as $source)
        {
            $sourceAlias = $source['alias'];
            /** @var DataSourceMetadata $sourceMetadata  */
            $sourceMetadata = $source['metadata'];
            $sourceSqlAlias = 't' . $sourceCount;

            if ($defaultTable == null)
                $defaultTable = $sourceAlias;

            $sourceAliasToSqlAlias[$sourceAlias] = $sourceSqlAlias;
            $sourceCount++;

            foreach($sourceMetadata->GetFieldNames() as $fieldName)
                $map[PgqlUtils::MakeFullName($sourceAlias, $fieldName)] = SqlUtils::MakeFullName($sourceSqlAlias, $fieldName);
        }

        foreach($this->joins as $join)
        {
            $joinAlias = $join['alias'];

            /** @var DataSourceMetadata $joinMetadata */
            $joinMetadata = $join['metadata'];
            $joinSqlAlias = 't' . $sourceCount;

            $sourceAliasToSqlAlias[$joinAlias] = $joinSqlAlias;

            foreach($joinMetadata->GetFieldNames() as $fieldName)
                $map[PgqlUtils::MakeFullName($joinAlias, $fieldName)] = SqlUtils::MakeFullName($joinSqlAlias, $fieldName);

            $sourceCount++;
        }
        return array('defaultTable' => $defaultTable, 'map' => $map);
    }

    public final function GetSql()
    {
        $builder = new SelectCommandBuilder();
        $sourceCount = 0;

        $sourceAliasToSqlAlias = array();

        foreach($this->sources as $source)
        {
            $sourceAlias = $source['alias'];

            /** @var DataSourceMetadata $sourceMetadata  */
            $sourceMetadata = $source['metadata'];
            $sourceSqlAlias = 't' . $sourceCount;

            $sourceAliasToSqlAlias[$sourceAlias] = $sourceSqlAlias;

            $builder->AddSourceTable(
                $sourceMetadata->GetSourceSql(),
                $sourceSqlAlias
            );
            $sourceCount++;

            foreach($sourceMetadata->GetFieldNames() as $fieldName)
                $builder->AddField($fieldName, $sourceSqlAlias, $fieldName);
        }

        foreach($this->joins as $join)
        {
            $joinAlias = $join['alias'];

            /** @var DataSourceMetadata $joinMetadata */
            $joinMetadata = $join['metadata'];
            $joinSqlAlias = 't' . $sourceCount;

            $sourceAliasToSqlAlias[$joinAlias] = $joinSqlAlias;

            list($leftTable, $leftField) = PgqlUtils::SplitFullName($join['left']);
            list($rightTable, $rightField) = PgqlUtils::SplitFullName($join['right']);
            
            $condition = new SMCondition(
                $builder->CreateFieldInfo($leftField, $sourceAliasToSqlAlias[$leftTable]),
                ConditionOperator::$Equals,
                $builder->CreateFieldInfo($rightField, $sourceAliasToSqlAlias[$rightTable])
            );

            $builder->AddJoinTable(JoinType::$Inner, $joinMetadata->GetSourceSql() , $joinSqlAlias, $condition);

            foreach($joinMetadata->GetFieldNames() as $fieldName)
                $builder->AddField($fieldName, $joinSqlAlias, $joinSqlAlias . '_' . $fieldName);

            $sourceCount++;
        }


        $fieldMap = $this->GetInternalFieldsMap();
        foreach($this->conditions as $condition)
        {
            $left = $condition['left'];
            $right = $condition['right'];
            $operator = $condition['operator'];

            list($leftTableName, $leftFieldName) = PgqlUtils::SplitFullName($left);
            if (StringUtils::IsNullOrEmpty($leftTableName))
                $leftTableName = $fieldMap['defaultTable'];
            $left = PgqlUtils::MakeFullName($leftTableName, $leftFieldName);

            $builder->AddCondition(new SMCondition($fieldMap['map'][$left], $operator, $right));
        }

        return $builder->GetSql();
    }

    /**
     * @param IConnection $connection
     * @return IDataReader
     */
    public final function ExecuteReader(IConnection $connection)
    {
        return
            new MapFieldsDataReaderDecorator(
                $connection->ExecuteReader($this->GetSql()),
                $this->GetFieldsMap()
            );
    }

    private $conditions = array();

    public final function AddSimpleCondition($left, ConditionOperator $operator, $right)
    {
        $this->conditions[] = array(
            'left' => $left,
            'right' => $right,
            'operator' => $operator
        );
    }
}

class DataReaderDecorator implements IDataReader
{
    /** @var \IDataReader */
    private $dataReader;

    /**
     * @param IDataReader $dataReader
     */
    public function __construct(IDataReader $dataReader)
    {
        $this->dataReader = $dataReader;
    }

    /**
     * @return void
     */
    function Close()
    {
        $this->dataReader->Close();
    }

    /**
     * @param string $fieldName
     * @return mixed
     */
    function GetFieldValueByName($fieldName)
    {
        return $this->dataReader->GetFieldValueByName($fieldName);
    }

    /**
     * @return boolean
     */
    function Next()
    {
        return $this->dataReader->Next();
    }

    /**
     * @return void
     */
    function Open()
    {
        $this->dataReader->Open();
    }

    /**
     * @return string[]
     */
    function GetFieldNames()
    {
        return $this->dataReader->GetFieldNames();
    }

    /**
     * @param string $fieldName
     * @return boolean
     */
    function IsPrimaryKeyField($fieldName)
    {
        return $this->dataReader->IsPrimaryKeyField($fieldName);
    }
}

class MapFieldsDataReaderDecorator extends DataReaderDecorator
{
    private $fieldMap;

    public function __construct(IDataReader $dataReader, $fieldMap)
    {
        parent::__construct($dataReader);
        $this->fieldMap = $fieldMap;
    }

    /**
     * @param string $fieldName
     * @return mixed
     */
    public function GetFieldValueByName($fieldName)
    {
        list($tableName, $fieldName) = PgqlUtils::SplitFullName($fieldName);
        if (StringUtils::IsNullOrEmpty($tableName))
            $tableName = $this->fieldMap['defaultTable'];
        $fullFieldName = PgqlUtils::MakeFullName($tableName, $fieldName);

        
        return parent::GetFieldValueByName($this->fieldMap['map'][$fullFieldName]);
    }
}

?>